import {
    getBusinessObject,
    is
} from 'bpmn-js/lib/util/ModelUtil';

import {
    isEventSubProcess,
    isExpanded
} from 'bpmn-js/lib/util/DiUtil';

import {
    isDifferentType
} from 'bpmn-js/lib/features/popup-menu/util/TypeUtil';

import {
    forEach,
    filter
} from 'min-dash';

import * as replaceOptions from './CustomReplaceOptions';


/**
 * This module is an element agnostic replace menu provider for the popup menu.
 */
export default function CustomReplaceMenuProvider(
    bpmnFactory, popupMenu, modeling, moddle,
    bpmnReplace, rules, translate) {

    this._bpmnFactory = bpmnFactory;
    this._popupMenu = popupMenu;
    this._modeling = modeling;
    this._moddle = moddle;
    this._bpmnReplace = bpmnReplace;
    this._rules = rules;
    this._translate = translate;

    this.register();
}

CustomReplaceMenuProvider.$inject = [
    'bpmnFactory',
    'popupMenu',
    'modeling',
    'moddle',
    'bpmnReplace',
    'rules',
    'translate'
];


/**
 * Register replace menu provider in the popup menu
 */
CustomReplaceMenuProvider.prototype.register = function() {
    this._popupMenu.registerProvider('bpmn-replace', this);
};


/**
 * Get all entries from replaceOptions for the given element and apply filters
 * on them. Get for example only elements, which are different from the current one.
 *
 * @param {djs.model.Base} element
 *
 * @return {Array<Object>} a list of menu entry items
 */
CustomReplaceMenuProvider.prototype.getEntries = function(element) {

    var businessObject = element.businessObject;

    var rules = this._rules;

    var entries;

    if (!rules.allowed('shape.replace', { element: element })) {
        return [];
    }

    var differentType = isDifferentType(element);

    if (is(businessObject, 'bpmn:DataObjectReference')) {

        if (businessObject.type) {
            return this._createEntries(element, replaceOptions.IOT_REFERENCE);
        }
        return this._createEntries(element, replaceOptions.DATA_OBJECT_REFERENCE);
    }

    if (is(businessObject, 'bpmn:DataStoreReference')) {
        return this._createEntries(element, replaceOptions.DATA_STORE_REFERENCE);
    }

    // start events outside sub processes
    if (is(businessObject, 'bpmn:StartEvent') && !is(businessObject.$parent, 'bpmn:SubProcess')) {
        //TODO: filter current selected element (optional :D)
        //entries = filter(replaceOptions.START_EVENT, differentType);
        entries = replaceOptions.START_EVENT;

        return this._createEntries(element, entries);
    }

    // expanded/collapsed pools
    if (is(businessObject, 'bpmn:Participant')) {

        entries = filter(replaceOptions.PARTICIPANT, function(entry) {
            return isExpanded(businessObject) !== entry.target.isExpanded;
        });

        return this._createEntries(element, entries);
    }

    // start events inside event sub processes
    if (is(businessObject, 'bpmn:StartEvent') && isEventSubProcess(businessObject.$parent)) {
        entries = filter(replaceOptions.EVENT_SUB_PROCESS_START_EVENT, function(entry) {

            var target = entry.target;

            var isInterrupting = target.isInterrupting !== false;

            var isInterruptingEqual = getBusinessObject(element).isInterrupting === isInterrupting;

            // filters elements which types and event definition are equal but have have different interrupting types
            return differentType(entry) || !differentType(entry) && !isInterruptingEqual;

        });

        return this._createEntries(element, entries);
    }

    // start events inside sub processes
    if (is(businessObject, 'bpmn:StartEvent') && !isEventSubProcess(businessObject.$parent)
        && is(businessObject.$parent, 'bpmn:SubProcess')) {
        entries = filter(replaceOptions.START_EVENT_SUB_PROCESS, differentType);

        return this._createEntries(element, entries);
    }

    // end events
    if (is(businessObject, 'bpmn:EndEvent')) {

        entries = filter(replaceOptions.END_EVENT, function(entry) {
            var target = entry.target;

            // hide cancel end events outside transactions
            if (target.eventDefinitionType == 'bpmn:CancelEventDefinition' && !is(businessObject.$parent, 'bpmn:Transaction')) {
                return false;
            }

            return differentType(entry);
        });

        return this._createEntries(element, entries);
    }

    // boundary events
    if (is(businessObject, 'bpmn:BoundaryEvent')) {

        entries = filter(replaceOptions.BOUNDARY_EVENT, function(entry) {

            var target = entry.target;

            if (target.eventDefinitionType == 'bpmn:CancelEventDefinition' &&
                !is(businessObject.attachedToRef, 'bpmn:Transaction')) {
                return false;
            }
            var cancelActivity = target.cancelActivity !== false;

            var isCancelActivityEqual = businessObject.cancelActivity == cancelActivity;

            return differentType(entry) || !differentType(entry) && !isCancelActivityEqual;
        });

        return this._createEntries(element, entries);
    }

    // intermediate events
    if (is(businessObject, 'bpmn:IntermediateCatchEvent') ||
        is(businessObject, 'bpmn:IntermediateThrowEvent')) {

        entries = filter(replaceOptions.INTERMEDIATE_EVENT, differentType);

        return this._createEntries(element, entries);
    }

    // gateways
    if (is(businessObject, 'bpmn:Gateway')) {

        entries = filter(replaceOptions.GATEWAY, differentType);

        return this._createEntries(element, entries);
    }

    // transactions
    if (is(businessObject, 'bpmn:Transaction')) {

        entries = filter(replaceOptions.TRANSACTION, differentType);

        return this._createEntries(element, entries);
    }

    // expanded event sub processes
    if (isEventSubProcess(businessObject) && isExpanded(businessObject)) {

        entries = filter(replaceOptions.EVENT_SUB_PROCESS, differentType);

        return this._createEntries(element, entries);
    }

    // expanded sub processes
    if (is(businessObject, 'bpmn:SubProcess') && isExpanded(businessObject)) {

        entries = filter(replaceOptions.SUBPROCESS_EXPANDED, differentType);

        return this._createEntries(element, entries);
    }

    // collapsed ad hoc sub processes
    if (is(businessObject, 'bpmn:AdHocSubProcess') && !isExpanded(businessObject)) {

        entries = filter(replaceOptions.TASK, function(entry) {

            var target = entry.target;

            var isTargetSubProcess = target.type === 'bpmn:SubProcess';

            var isTargetExpanded = target.isExpanded === true;

            return isDifferentType(element, target) && (!isTargetSubProcess || isTargetExpanded);
        });

        return this._createEntries(element, entries);
    }

    // sequence flows
    if (is(businessObject, 'bpmn:SequenceFlow')) {
        return this._createSequenceFlowEntries(element, replaceOptions.SEQUENCE_FLOW);
    }

    // flow nodes
    if (is(businessObject, 'bpmn:FlowNode')) {
        entries = filter(replaceOptions.TASK, differentType);

        // collapsed SubProcess can not be replaced with itself
        if (is(businessObject, 'bpmn:SubProcess') && !isExpanded(businessObject)) {
            entries = filter(entries, function(entry) {
                return entry.label !== 'Sub Process (collapsed)';
            });
        }

        return this._createEntries(element, entries);
    }

    return [];
};


/**
 * Get a list of header items for the given element. This includes buttons
 * for multi instance markers and for the ad hoc marker.
 *
 * @param {djs.model.Base} element
 *
 * @return {Array<Object>} a list of menu entry items
 */
CustomReplaceMenuProvider.prototype.getHeaderEntries = function(element) {

    var headerEntries = [];

    if (is(element, 'bpmn:Activity') && !isEventSubProcess(element)) {
        headerEntries = headerEntries.concat(this._getLoopEntries(element));
    }
    // show collection header if it is not iot
    if (is(element, 'bpmn:DataObjectReference') && !getBusinessObject(element).get('type')) {
        headerEntries = headerEntries.concat(this._getDataObjectIsCollection(element));
    }

    if (is(element, 'bpmn:Participant')) {
        headerEntries = headerEntries.concat(this._getParticipantMultiplicity(element));
    }

    if (is(element, 'bpmn:SubProcess') &&
        !is(element, 'bpmn:Transaction') &&
        !isEventSubProcess(element)) {
        headerEntries.push(this._getAdHocEntry(element));
    }

    return headerEntries;
};


/**
 * Creates an array of menu entry objects for a given element and filters the replaceOptions
 * according to a filter function.
 *
 * @param  {djs.model.Base} element
 * @param  {Object} replaceOptions
 *
 * @return {Array<Object>} a list of menu items
 */
CustomReplaceMenuProvider.prototype._createEntries = function(element, replaceOptions) {
    var menuEntries = [];

    var self = this;

    forEach(replaceOptions, function(definition) {
        var entry = self._createMenuEntry(definition, element);

        menuEntries.push(entry);
    });
    return menuEntries;
};

/**
 * Creates an array of menu entry objects for a given sequence flow.
 *
 * @param  {djs.model.Base} element
 * @param  {Object} replaceOptions

 * @return {Array<Object>} a list of menu items
 */
CustomReplaceMenuProvider.prototype._createSequenceFlowEntries = function(element, replaceOptions) {

    var businessObject = getBusinessObject(element);

    var menuEntries = [];

    var modeling = this._modeling,
        moddle = this._moddle;

    var self = this;

    forEach(replaceOptions, function(entry) {

        switch (entry.actionName) {
            case 'replace-with-default-flow':
                if (businessObject.sourceRef.default !== businessObject &&
                    (is(businessObject.sourceRef, 'bpmn:ExclusiveGateway') ||
                        is(businessObject.sourceRef, 'bpmn:InclusiveGateway') ||
                        is(businessObject.sourceRef, 'bpmn:ComplexGateway') ||
                        is(businessObject.sourceRef, 'bpmn:Activity'))) {

                    menuEntries.push(self._createMenuEntry(entry, element, function() {
                        modeling.updateProperties(element.source, { default: businessObject });
                    }));
                }
                break;
            case 'replace-with-conditional-flow':
                if (!businessObject.conditionExpression && is(businessObject.sourceRef, 'bpmn:Activity')) {

                    menuEntries.push(self._createMenuEntry(entry, element, function() {
                        var conditionExpression = moddle.create('bpmn:FormalExpression', { body: '' });

                        modeling.updateProperties(element, { conditionExpression: conditionExpression });
                    }));
                }
                break;
            default:

                // default flows
                if (is(businessObject.sourceRef, 'bpmn:Activity') && businessObject.conditionExpression) {
                    return menuEntries.push(self._createMenuEntry(entry, element, function() {
                        modeling.updateProperties(element, { conditionExpression: undefined });
                    }));
                }

                // conditional flows
                if ((is(businessObject.sourceRef, 'bpmn:ExclusiveGateway') ||
                        is(businessObject.sourceRef, 'bpmn:InclusiveGateway') ||
                        is(businessObject.sourceRef, 'bpmn:ComplexGateway') ||
                        is(businessObject.sourceRef, 'bpmn:Activity')) &&
                    businessObject.sourceRef.default === businessObject) {

                    return menuEntries.push(self._createMenuEntry(entry, element, function() {
                        modeling.updateProperties(element.source, { default: undefined });
                    }));
                }
        }
    });

    return menuEntries;
};


/**
 * Creates and returns a single menu entry item.
 *
 * @param  {Object} definition a single replace options definition object
 * @param  {djs.model.Base} element
 * @param  {Function} [action] an action callback function which gets called when
 *                             the menu entry is being triggered.
 *
 * @return {Object} menu entry item
 */
CustomReplaceMenuProvider.prototype._createMenuEntry = function(definition, element, action) {
    var translate = this._translate;
    var replaceElement = this._bpmnReplace.replaceElement;

    var replaceAction = function() {
        return replaceElement(element, definition.target);
    };

    var label = definition.label;
    if (label && typeof label === 'function') {
        label = label(element);
    }

    action = action || replaceAction;

    var menuEntry = {
        label: translate(label),
        className: definition.className,
        id: definition.actionName,
        //custom
        iot: definition.iot || null,
        action: action
    };

    return menuEntry;
};

/**
 * Get a list of menu items containing buttons for multi instance markers
 *
 * @param  {djs.model.Base} element
 *
 * @return {Array<Object>} a list of menu items
 */
CustomReplaceMenuProvider.prototype._getLoopEntries = function(element) {

    var self = this;
    var translate = this._translate;

    function toggleLoopEntry(event, entry) {
        var loopCharacteristics;

        if (entry.active) {
            loopCharacteristics = undefined;
        } else {
            loopCharacteristics = self._moddle.create(entry.options.loopCharacteristics);

            if (entry.options.isSequential) {
                loopCharacteristics.isSequential = entry.options.isSequential;
            }
        }
        self._modeling.updateProperties(element, { loopCharacteristics: loopCharacteristics });
    }

    var businessObject = getBusinessObject(element),
        loopCharacteristics = businessObject.loopCharacteristics;

    var isSequential,
        isLoop,
        isParallel;

    if (loopCharacteristics) {
        isSequential = loopCharacteristics.isSequential;
        isLoop = loopCharacteristics.isSequential === undefined;
        isParallel = loopCharacteristics.isSequential !== undefined && !loopCharacteristics.isSequential;
    }


    var loopEntries = [
        {
            id: 'toggle-parallel-mi',
            className: 'bpmn-icon-parallel-mi-marker',
            title: translate('Parallel Multi Instance'),
            active: isParallel,
            action: toggleLoopEntry,
            options: {
                loopCharacteristics: 'bpmn:MultiInstanceLoopCharacteristics',
                isSequential: false
            }
        },
        {
            id: 'toggle-sequential-mi',
            className: 'bpmn-icon-sequential-mi-marker',
            title: translate('Sequential Multi Instance'),
            active: isSequential,
            action: toggleLoopEntry,
            options: {
                loopCharacteristics: 'bpmn:MultiInstanceLoopCharacteristics',
                isSequential: true
            }
        },
        {
            id: 'toggle-loop',
            className: 'bpmn-icon-loop-marker',
            title: translate('Loop'),
            active: isLoop,
            action: toggleLoopEntry,
            options: {
                loopCharacteristics: 'bpmn:StandardLoopCharacteristics'
            }
        }
    ];
    return loopEntries;
};

/**
 * Get a list of menu items containing a button for the collection marker
 *
 * @param  {djs.model.Base} element
 *
 * @return {Array<Object>} a list of menu items
 */
CustomReplaceMenuProvider.prototype._getDataObjectIsCollection = function(element) {

    var self = this;
    var translate = this._translate;

    function toggleIsCollection(event, entry) {
        self._modeling.updateModdleProperties(
            element,
            dataObject,
            { isCollection: !entry.active });
    }

    var dataObject = element.businessObject.dataObjectRef,
        isCollection = dataObject.isCollection;

    var dataObjectEntries = [
        {
            id: 'toggle-is-collection',
            className: 'bpmn-icon-parallel-mi-marker',
            title: translate('Collection'),
            active: isCollection,
            action: toggleIsCollection,
        }
    ];
    return dataObjectEntries;
};

/**
 * Get a list of menu items containing a button for the participant multiplicity marker
 *
 * @param  {djs.model.Base} element
 *
 * @return {Array<Object>} a list of menu items
 */
CustomReplaceMenuProvider.prototype._getParticipantMultiplicity = function(element) {

    var self = this;
    var bpmnFactory = this._bpmnFactory;
    var translate = this._translate;

    function toggleParticipantMultiplicity(event, entry) {
        var isActive = entry.active;
        var participantMultiplicity;

        if (!isActive) {
            participantMultiplicity = bpmnFactory.create('bpmn:ParticipantMultiplicity');
        }

        self._modeling.updateProperties(
            element,
            { participantMultiplicity: participantMultiplicity });
    }

    var participantMultiplicity = element.businessObject.participantMultiplicity;

    var participantEntries = [
        {
            id: 'toggle-participant-multiplicity',
            className: 'bpmn-icon-parallel-mi-marker',
            title: translate('Participant Multiplicity'),
            active: !!participantMultiplicity,
            action: toggleParticipantMultiplicity,
        }
    ];
    return participantEntries;
};


/**
 * Get the menu items containing a button for the ad hoc marker
 *
 * @param  {djs.model.Base} element
 *
 * @return {Object} a menu item
 */
CustomReplaceMenuProvider.prototype._getAdHocEntry = function(element) {
    var translate = this._translate;
    var businessObject = getBusinessObject(element);

    var isAdHoc = is(businessObject, 'bpmn:AdHocSubProcess');

    var replaceElement = this._bpmnReplace.replaceElement;

    var adHocEntry = {
        id: 'toggle-adhoc',
        className: 'bpmn-icon-ad-hoc-marker',
        title: translate('Ad-hoc'),
        active: isAdHoc,
        action: function(event, entry) {
            if (isAdHoc) {
                return replaceElement(element, { type: 'bpmn:SubProcess' }, {
                    autoResize: false,
                    layoutConnection: false
                });
            } else {
                return replaceElement(element, { type: 'bpmn:AdHocSubProcess' }, {
                    autoResize: false,
                    layoutConnection: false
                });
            }
        }
    };

    return adHocEntry;
};
